Foreword
I was just finishing this article off when Manuel Abadia released an article with the same name ('Two way data binding in ASP.NET'), topic and (perhaps not surprisingly) some of the same class names. I'd been gazumped (if not just plain beaten)! This came as a bit of a shock, but I've decided to release this anyway since our approaches are slightly different and hopefully between the two people may find something useful. Unfortunately the submission of this article got delayed by about eight months whilst I went traveling (I'm finishing it off on the laptop camped by a creek in Australia's NT), so it's not quite as 'hot off the press' as it once was, and is almost obsolete already (since VS 2005 supports two-way binding). Ho hum.
Still, for what it's worth, here's how to implement the "Two-way databinding in ASP.NET".
NB: Throughout the article where I refer to 'databinding' without specifying which type (simple or complex) I'm referring to simple databinding.
Introduction
This article describes a two-way databinding scheme for ASP.NET, which extends the built-in simple databinding support to allow for automatic updates back to the original DataSource(s). The intent was to support a scheme that - like the built-in simple databinding - required no support from the bound controls themselves, rather was a 'framework' functionality available through a Page
class. Additionally I hoped to design a scheme that would work similar to how a two-way databinding in VS 2005 would work when released (which I still haven't got round to verifying yet) so pages should require minimum work to port over.
Background
After my initial honeymoon period with ASP.NET had elapsed, the absence of two-way binding was the first of various 'features' to drive me back into slightly more critical mode, but I never got round to doing anything about it. I was eventually goaded into action by various articles describing 'the answer' as being sub classing all of your controls to support databinding intrinsically. This would have been bearable, had I not then ended up working for a company in which the developers had done exactly that, and it wasn't a nice sight.
Now just in case anyone's wondering what's wrong with the 'subclass everything' approach:
- It's highly labour intensive.
- It pre-supposes the controls you wish to use aren't
sealed
, and are amenable to sub classing in this manner. If not then you're going to have to compose them and delegate most of the functionality.
- It's not integrated with the built in binding support in the designer.
The first is, surely, the killer. For every type of control you want to use in your two-way binding you're going to subclass it? You've got to be kidding. It defeats the whole point of having reusable controls in the first place.
The real solution, surely, is to come up with a scheme that can take the existing databinding information and perform its function in reverse, and that's what I set about. My solution had to be along the lines of how I thought Microsoft would have probably done it had they had more time, which became especially pertinent when I learnt that Whidbey will (should) support two-way databinding - I'm hoping pages built using this system will just 'slot in' to Whidbey when it comes out with minimum tweaking.
As I saw it, there were three main parts to the solution:
First, I needed to make sure I understood what happened normally. Along the way I learnt some things about simple databinding that I didn't know or had forgotten, so even guru's might learn something in this next section.
Simple DataBinding - a (brief) refresher
Simple databinding is set up by entering databinding expressions into the relevant attribute of the control's ASPX tag. So to bind the Text
property of a TextBox
'txtName
' to DataSet
'demoData1
', table 'Table1
', column 'Name
', you'd do something like this:
<asp:TextBox id="txtName" Runat="server"
Text='<%# DataBinder.Eval(demoData1,
"Tables[Table1].DefaultView.[0].Name") %>'/>
These databinding expressions work for all System.UI.Web.Control
s, but are only supported in the designer for controls derived from System.UI.Web.WebControls.WebControl
. The designer support (apart from not showing the databinding expression as the contents of the control) provides the ability to create and edit the binding expressions without having to go into the ASPX source code - just select the ellipsis ('...') next to (DataBindings) in the control's property grid.
The designer can do this because all Control
s implement IDataBindingsAccessor
, which allows access to a DataBindingCollection
detailing which properties of that control are bound to what expressions. Unfortunately (and this would be a very short article otherwise) this collection is only available at design time, having been dynamically built from the ASPX source when the page loads in the designer. The actual persistence format for the databinding information is that embedded in the ASPX source as shown above.
This is important because it enables databinding to be performed with a simple, declarative syntax, and doesn't require the use of Visual Studio .NET. However from the point of view of re-using that binding information it leaves a lot to be desired.
So what happens at run-time? If you'd hoped that all that binding information would get parsed by the page builder and stored somewhere, you'd be wrong. The ultimate destination for that information is the dynamically-generated Page
class, where each control's bindings are used to generate a method that's hooked to the relevant control's DataBind
event:
[An example of the auto-generated code generated from your ASPX page at runtime:]
private System.Web.UI.Control __BuildControltxtName() {
#line 18 "c:\inetpub\wwwroot\TwoWayDataBindingDemo\BuiltInSimpleBinding.aspx"
__ctrl.DataBinding +=
new System.EventHandler(this.__DataBindtxtName);
return __ctrl;
}
public void __DataBindtxtName(object sender, System.EventArgs e) {
System.Web.UI.Control Container;
System.Web.UI.WebControls.TextBox target;
#line 18 "c:\inetpub\wwwroot\TwoWayDataBindingDemo\BuiltInSimpleBinding.aspx"
target = ((System.Web.UI.WebControls.TextBox)(sender));
#line default
#line 18 "c:\inetpub\wwwroot\TwoWayDataBindingDemo\BuiltInSimpleBinding.aspx"
Container = ((System.Web.UI.Control)(target.BindingContainer));
#line default
#line 18 "c:\inetpub\wwwroot\TwoWayDataBindingDemo\BuiltInSimpleBinding.aspx"
target.Text = System.Convert.ToString(DataBinder.Eval(demoData1,
"Tables[Table1].DefaultView.[0].Name"));
#line default
}
There're two things to note here:
- We're not going to be able to do anything useful with this. The binding information is buried in the source and it would be pretty nasty to have to extract it.
- The inline type conversion that happens at binding is not the smartest. Apart from
Convert.ToString()
, it pretty much assumes that the data value can be cast straight to the control property type. Having spent a lot of time implementing TypeConverter
s for better design-time support I'd rather hoped for something better when it came to databinding - as much as anything it falls over for DBNull
, which is not uncommon.
- Most of the work is still being delegated to
Databinder.Eval()
.
Retrieving the binding information
As a result of the above it became clear that one way or another the binding information had to be loaded back into the page. Since it was stored in the ASPX source, I was going to parse the source and retrieve it. Fortunately I'd already written a class that made this a lot easier - HtmlTag
- so all I had to do was:
- Load up the ASPX source for the page.
- Create
HtmlTag
instances for each tag in the document source.
(Author's note: I never really liked this, and ManuDev's solution of using a custom control to persist the binding information somewhere else on the page at design time to make it available for runtime is much much neater. It does suffer, however, in that it requires Visual Studio .NET and duplicates the binding information (so HTML-level code edits need to be done in two places if you're not going to go back through the designer). Nevertheless its generally more palatable.)
To store the binding information, I created a class DataBindingInfo
which represents an individual attribute binding on a given server side control. DataBindingInfo
stores the binding expression already 'split up', so our example binding:
<asp:TextBox id="txtName" Runat="server"
Text='<%# DataBinder.Eval(demoData1,
"Tables[Table1].DefaultView.[0].Name") %>'/>
would be used to generate a DataBindingInfo
like this:
new DataBindingInfo(txtName.ID, "Text", "demoData1",
"Tables[Table1].DefaultView.[0].Name", "");
where txtName
is the server side control generated from the tag at runtime, "demoData1
" will later on be resolved to a property demoData1
on the Page
class and the final empty string
argument represents DataBinder.Eval()
's optional format string
argument (in fact in the code sample you won't see the constructor being explicitly called like this, since DataBindingInfoCollection
has an overloaded Add()
method that takes the same arguments and calls the constructor for you).
The job was then simply to populate a collection of these from the binding information parsed from the source.
I'm not going to go into too much detail about this, because all I actually did was to re-use an existing class I'd written - HtmlTag
- to look for tags in the source that had attributes that matched the databinding syntax (expressed in the Regex dataBoundAttributeMatcher
). For those I used the HtmlTag
's ID
property to find the control instance using page.FindControl()
and created a new DataBindingInfo
from the combination of the control, the name of the attribute (which maps to a property on the control) being bound and the binding expression that is being used for the binding (split as detailed above).
The code ended up looking like this (please note that System.Web.UI
has been aliased to the WebUI
namespace in this sample):
private DataBindingInfoCollection CreateDataBindings() {
DataBindingInfoCollection bindings = new DataBindingInfoCollection();
string pageSource =GetFileContents(page.Request.PhysicalPath);
MatchCollection matches =new Regex(HtmlParsing.RegExPatterns.HtmlTag,
RegexOptions.Compiled | RegexOptions.IgnoreCase).Matches(pageSource);
HtmlParsing.HtmlTag tag;
foreach(Match tagMatch in matches){
tag =new HtmlParsing.HtmlTag(tagMatch.Value);
AddBindingsForTag(tag, bindings);
}
return bindings;
}
private void AddBindingsForTag(HtmlParsing.HtmlTag tag,
DataBindingInfoCollection bindings){
string attribName, attribValue;
BindingExpression attribExpression;
DataBindingInfo bindingInfo;
if (tag.ID!=String.Empty){
Control control =page.FindControl(tag.ID);
if (control!=null){
foreach(System.Collections.DictionaryEntry item in tag.Attributes){
attribName =item.Key.ToString();
attribValue =item.Value.ToString();
if (attribValue!=String.Empty &&
attribValue.StartsWith(webBindingPrefix) &&
attribValue.EndsWith(webBindingSuffix)){
attribValue = attribValue.Substring(webBindingPrefix.Length,
attribValue.Length - webBindingPrefix.Length -
webBindingSuffix.Length);
attribExpression =BindingExpression.Parse(attribValue);
if (attribExpression!=null){
bindingInfo =bindings.Add(control.ID, attribName,
attribExpression.DataSource,
attribExpression.DataExpression,
attribExpression.FormatString);
bindingInfo.IsTwoWay =
IsTwoWayProperty(attribName, control);
}
}
}
}
}
}
(The source for the HtmlTag
is included with the demo project if you're interested - it's just a wrapper over a couple of RegEx's).
For performance reasons, the DataBindingInfoCollection
for any given page is stuffed in the ASP Cache
object, so it's not always being re-calculated:
public DataBindingInfoCollection GetDataBindings(){
DataBindingInfoCollection bindings =
page.Cache[CacheKey] as DataBindingInfoCollection;
if (bindings==null){
bindings =CreateDataBindings();
CacheDependency depends =new CacheDependency(page.Request.PhysicalPath);
page.Cache.Add(CacheKey, bindings, depends, Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration, CacheItemPriority.BelowNormal, null);
}
return bindings;
}
Now I always had a few reservations about having to do this, so in fact this is just one possible strategy that a DataBoundPage
can use to retrieve its DataBindingInfoCollection
. Specifically the derived class can override the BindingInfoProvider
property to return an IDataBindingInfoProvider
of its choice. I've been experimenting with one that collects the binding information at design-time in the IDE - DesignerBindingInfoProvider
- which is included, but not yet entirely functional. This is implemented as a functioning IExtenderProvider
, thanks to Wouter van Vugt's article 'ASPExtenderSerializer', which provides a custom CodeDomSerializer
to replace VS 2002/3's buggy one.
Unfortunately it looks like this will stop working in VS 2005, because IComponents aren't properly supported in the ASP.NET designer any more. This is a bigger issue than this soon-to-be-obsolete article, so start complaining. I want my IExtenderProviders
...
Resurrecting the DataSource
Typically the original DataSource is created in a fairly ad-hoc manner, just before consumption, normally something like this:
private void Page_Load(object sender, System.EventArgs e)
{
if (!IsPostBack){
dataAdapter.Fill(dataSet1);
DataBind();
}
}
Once bound the controls retain what data was bound into them (for postbacks), but the original DataSet
is not persisted automatically. This is intentional - DataSet
s can be quite large structures, with lots of metadata and versioning information, and to persist it (either in ViewState or in Session) would have performance implications (not to mention there being no easy way to determine which DataSet
s to persist and which were transient).
What this does mean is that before we unbind the data, we've got to get the DataSet
back. I've provided a framework for doing this in the DataBoundPage
class. This defines a template method DataUnbind
for the steps you need to perform for reverse databinding. Specifically implementing pages should override EnsureDataSource()
to re-create the DataSet
(if necessary) as this method is called prior to performing 'unbinding'. You worry about getting the DataSet
back, we'll do the rest:
protected virtual void DataUnbind(){
EnsureDataBindings();
EnsureDataSource();
OnDataUnbinding();
AutoDataUnbind();
}
Unfortunately without the original DataSet
you've lost all your concurrency protection. If you merely implement EnsureDataSource()
to essentially re-run the original query again and the data has been changed, the 'original' row in our new dataset will contain the newly changed data, so the concurrency violation won't get caught automatically and we'll overwrite someone else's changes.
So what we need is a space-efficient way of persisting the original DataSet
, complete with original/modified copies of changed rows, for which there are essentially two options:
- Save the original
DataSet
into ViewState, but Remove()
all the rows other than the bound row first to save space.
- Save an XML
DiffGram
of any bound rows, and use it to re-create just those rows in the DataSet
later.
- Re-generate the
DataSet
from the DataSource, but manually check for concurrency issues at the point you load the data. This is simplest if your database supports a timestamp column of some nature that can be used as a version marker for a given row.
- Store a version marker (or a hash) of the row in ViewState and implement your own concurrency handling.
Which you choose will depend on your exact needs, and whether you really want to have to tinker with all of those IDE-generated stored procs in your database or not.
TwoWayDataBinder.Eval()
This is, of course, the interesting bit. We've retrieved all the binding information and resurrected the DataSource, but without a way of being able to re-map the bound control data back from where it came we've achieved nothing. What we need is to be able to replicate DataBinder.Eval()
's behaviour, but in reverse.
To recap, the DataBinder.Eval()
method performs the handy task of taking an object
reference and a string
expression and 'walks' the expression, resolving each part into a property (or indexer) on the object, retrieving its value and then using that value to recourse down into the next part of the expression. That's a complicated way of explaining something that's quite intuitive to understand when you see what it does (pseudo code):
DataBinder.Eval(obj1, "property1.property2")
It's actually fairly easy to write some code to do all this (using reflection), and so it's fairly easy to write some code that does exactly the same but when it reaches the end of the expression it sets the property to a value passed in as an extra argument, rather than getting and returning the value of the property. Assuming we'd called this method DataBinder.UnEval()
, reversing the databinding would then be a case of looping through a DataBindingInfoCollection
, and performing the following steps to each item:
- Resolve the property on the control specified by the attribute name, and retrieve its value (e.g. for a
Textbox
typically you'd bind to the Text
property).
- Pass that value to
DataBinder.UnEval()
method along with the control reference and the binding expression. It'd then walk the binding expression and set the appropriate value in the data source to whatever Text
was set to.
However this produces a chicken-and-egg situation when it comes to type conversion. Say property2
is an Int32
. You've bound it to textBox.Text
just fine (since the built in databinding can stretch to turning an integer into a string), but to reverse the binding you've got to convert it back into an integer. However you don't know that property2
is an integer beforehand, because its only when you parse the expression that you get a reference to property2
, so how do you know what to convert it to? You could use DataBinder.Eval()
to get property2
's current value, then use the type
of that to convert Text
(if required) and then run DataBinder.UnEval()
, but it seems a bit wasteful to have to walk the expression twice (not to mention the problems you'd get if the initial value was null
).
What you need is a way of evaluating the expression and returning a reference to the property at the end of the expression, rather than its value. That way the expression parsing code gets run once, and the result can be used to get, set, and look at type information. But since .NET's reflection property handlers (PropertyInfo
and PropertyDescriptor
) are instance-less (that is to say once we had a reference to property2
we'd still need the value of property1
to actually get/set the property2
value) we're going to have to return both the property and the object
to which it refers.
For this I created IMemberInstance
. An IMemberInstance
represents a property reference bound up with the original object
instance on which the property can be found. As such an IMemberInstance
can be used to get/set the value of the property without having to supply the object instance to work on, which makes it ideal for a return value for our TwoWayDataBinder.Eval()
method. Additionally, since IMemberInstance
is fairly generic, it can be used as the common ground for different types of 'property instance' - i.e. IMemberInstance
implementations can use PropertyInfo
, PropertyDescriptor
or other mechanisms internally and it doesn't matter to the client. This was definitely a win-win, as we'll see later. I have covered this topic in more detail in a separate article about stateful reflection using IMemberInstance.
public interface IMemberInstance
{
object Instance{
get;
}
string Name {
get;
}
object Value {
get;
set;
}
Type Type {
get;
}
TypeConverter Converter {
get;
}
}
So now rather than writing an UnEval()
method, we're writing a replacement Eval()
method - TwoWayDataBinder.Eval()
- that returns IMemberInstance
rather than object
.
Performing a bind/unbind is then as simple as:
- Getting an
IMemberInstance
for the bound member on the D
ataBound
control (myTextBox.Text
for example).
- Getting an
IMemberInstance
for the member in the DataSource pointed to by the binding expression stored in our DataBindingInfo
(this is where TwoWayDataBinder.Eval()
comes in).
- Assigning the value of one to the other, with a little type conversion thrown in.
protected void AutoDataUnbind()
{
Debug.WriteLine("Unbinding data...");
foreach(DataBindingInfo info in DataBindings)
{
try
{
if (!info.IsTwoWay)
continue;
object boundObject = FindControl(info.BoundObject);
object bindingContainer =
GetBindingContainer(boundObject);
IMemberInstance boundProp =
TwoWayDataBinder.GetProperty(boundObject,
info.BoundMember);
object dataSource =
TwoWayDataBinder.GetField(bindingContainer,
info.DataSource).Value;
IMemberInstance dataMember = TwoWayDataBinder.Eval(dataSource,
info.Expression);
object typedValue;
if (info.HasConverter && info.Converter.CanConvertTo(dataMember.Type))
typedValue =
info.Converter.ConvertTo(boundProp.Value, dataMember.Type);
else if (dataMember.Type!=typeof(string) && (boundProp.Value==null ||
boundProp.Value.Equals(string.Empty)))
typedValue =info.EmptyValue;
else if (boundProp.Type.IsEnum && boundProp.Converter!=null &&
boundProp.Converter.CanConvertTo(dataMember.Type))
typedValue =boundProp.Converter.ConvertTo(boundProp.Value,
dataMember.Type);
else
typedValue =TwoWayDataBinder.ConvertType(boundProp.Value,
dataMember.Type, info.FormatString);
dataMember.Value =typedValue;
TraceBindingAssignment("Unbinding", typedValue, info);
}
catch(Exception err)
{
throw new ApplicationException(String.Format("Failed to set " +
"column {0} ({1})", info.Expression, err.Message), err);
}
}
}
Points of interest
When you really dig into the binding framework in .NET there's some quite clever stuff going on. There's also some serious omissions.
- Those declarative binding expressions are fine - because they don't force you to use VS - but they should go somewhere useful at runtime, and not just get plonked into a generated class.
ICustomTypeDescriptor
is pretty neat, but the ability for controls / DataSources to provide custom TypeConverter
s through it is totally ignored in ASP.NET databinding.
System.Web.UI.Control
s expose a property BindingContainer
that specifies their context as far as binding is concerned; that is to say controls nested in controls that are DataBound reference their parent as the binding container. Unfortunately this isn't documented anywhere (it's one of those 'this is for us, not for you' entries in MSDN).